關於 Design Principle 本篇將討論以下幾個問題
1. 關於 Design Principles
2. 關於 SOLID
3. Single-responsibility principle
4. Open–closed principle
5. Liskov substitution principle
6. Interface segregation principle
7. Dependency inversion principle
1. 關於 Design Principles
即便知道封裝、繼承、多型三大特性,也無法一下子就將程式寫得符合物件導向設計(Object-Oriented Programming, OOP),而設計原則可以協助我們以明確的定義作為標準,評斷程式中那些部分可以優化,以達到提高可讀性、維護性與擴充性的目的。
2. 關於 SOLID
SOLID 為設計原則中最廣為人知的五個原則,由大神 Robert C. Martin 在 2000 年的 paper 「Design Principles and Design Patterns」中介紹的(並非全都由 Robert C. Martin 提出),中文版可以參考無瑕的程式碼 敏捷完整篇。
SOLID 是由五個設計原則的字首(藏頭詩?)拼成,分別是
名稱(縮寫) | 中文 |
---|---|
Single-Responsibility Principle(SRP) | 單一職責原則 |
Open-Closed Principle(OCP) | 開閉原則 |
Liskov Substitution Principle(LSP) | 里氏替換原則 |
Interface Segregation Principles(ISP) | 介面隔離原則 |
Dependency Inversion Principle(DIP) | 依賴反轉原則 |
3. Single-responsibility principle
單一職責原則,一個 class 或模組應該只有一個職責
不同的層面來看同一個 class 或模組的職責是否足夠單一會有不同的結果(e.g. 從整個購物流程的層面來看,包含加入購物車 & 付款很合理,但從結帳流程來看,加入購物車卻顯得格格不入),而我們還是可以從一些比較客觀的方式來判斷
- class 或模組本身很龐大(e.g. 屬性或方法數量很多)
- class 或模組本身難以命名
- 依賴過多
4. Open–closed principle
開閉原則,對擴展開放,對修改封閉
要達到 OCP 最簡單的例子就是 abstract class 讓子類別 override,但其它時候呢?對擴展開發聽起來很合理,但對修改封閉卻很抽象,什麼樣的程度算是修改?是否有異動到物件本身就算是違反 OCP 呢?
實務上很難完全不會異動到物件本身,所以個人比較偏向這個看法
- 對擴展開放是為了因應變化
- 對修改封閉是為了保持原有程式的穩定
在這個前提之下,儘管我們在物件中新增了屬性、方法,但沒有修改到原有的程式,因應變化並保持原有程式穩定性,故沒有違反 OCP
5. Liskov substitution principle
里氏替換原則,子類別繼承父類別,可以修改實作,但不能改變父類別原有的約定(e.g. 回傳型別、參數、Exception的處理等)
有個簡單的驗證方式,將父類別的單元測試套用於子類別上,能通過單元測試通常都符合 LSP。違反 LSP 時未必都是子類別不遵守合約,也可能是抽象類別或介面設計上的錯誤,我們來看看實際的例子
- (O) 父類別定義方法
GetStartDate()
,子類別實作並回傳開始日期 - (X) 父類別定義方法
GetStartDate()
,子類別實作改為回傳結束日期 - (O) 父類別哺乳類,定義方法
Walk()
,子類別人類實作走路 - (X) 父類別哺乳類,定義方法
Walk()
,子類別鯨魚無法實作走路
6. Interface segregation principle
介面隔離原則,呼叫端不應該被強迫依賴它不需要的介面
ISP 跟 SRP 有點像,但角度有些不同,ISP 是由呼叫端的角度來判斷,SRP 則是關注在自己本身,而這邊說到的介面可以比較廣泛的解釋,像是
- interface 中有部分方法沒被呼叫端使用到,就應該拆分出來
- 呼叫端只需要一個
UserName
,但呼叫的方法 / API 卻回傳User
所有欄位回來
7. Dependency inversion principle
依賴反轉原則
High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
- 高階模組不應依賴於低階模組,兩個模組都應依賴抽象
- 抽像不應依賴細節,細節依賴於抽象
單看命名滿難理解 DIP 在說什麼,透過原文的說明可以知道,高階模組在呼叫低階模組時,應透過抽象,而不是直接呼叫低階模組的實作,藉此降低高低階模組間的耦合。
- 高階模組
DataTask
& 低階模組LogService
皆依賴 interfaceILogService
- 低階模組
LogService
就算沒有 interface 也不會反過來依賴高階模組DataTask
- 高階模組
DataTask
與低階模組LogService
之間為鬆耦合
public interface ILogService
{
void Info(DateTime time, StatusType status);
}
public class LogService : ILogService
{
public void Info(DateTime time, StatusType status)
{
// 實作寫 log
}
}
public class DataTask
{
private ILogService _log { get; }
public DataTask(ILogService logService)
{
_log = logService;
}
public void 處理資料排程()
{
_log.Info(DateTime.Now, StatusType.Start);
// 處理資料實作
_log.Info(DateTime.Now, StatusType.End);
}
}